Skip to content

Add Wayland docking support - #844

Merged
githubuser0xFFFF merged 1 commit into
githubuser0xFFFF:masterfrom
jetperch:wayland-standalone
Jun 15, 2026
Merged

Add Wayland docking support#844
githubuser0xFFFF merged 1 commit into
githubuser0xFFFF:masterfrom
jetperch:wayland-standalone

Conversation

@mliberty1

Copy link
Copy Markdown

Thanks for your great work on Qt-ADS! I have been using it in the Joulescope UI for many years, and it has been excellent. I have a number of customers affected by my UI issue #316, poor Wayland support. I tried to get someone on Upwork to fix this, but failed. Today, I worked with Claude Code / Fable to get this done.

image

This PR is a self-contained fix for Wayland. It's hopefully relatively easy to accept since most changes are gated to only affect Wayland. It does touch a bunch of files, though. The addition of GlobalPos parameter to a few methods should be a flow-through no-change on other platforms.

The overall experience is functional, but definitely not as slick as Windows and macOS. However, functional is an improvement and good enough to address my customers' concerns about slow performance using xcb.

Curated AI-generated summary follows:

Summary

This PR adds support for docking (undocking, moving, and re-docking floating widgets) on Wayland. It implements the approach outlined in #714 and follows the same mechanism that Qt itself uses for QDockWidget/QToolBar on Wayland.

Wayland forbids the techniques the docking system relies on elsewhere: a client cannot move its own top level windows in screen coordinates, cannot query the global cursor position, and cannot position one top level over another. Instead of the mouse tracked drag preview, undocking now performs a compositor driven drag:

  • Floating containers always use native window decorations on Wayland, so the compositor moves the window (the custom QWidget title bar cannot).
  • Undocking executes a QDrag carrying the MIME types that QWaylandDataDevice translates into an xdg_toplevel_drag_v1 request, so the real floating window follows the cursor (no translucent preview).
  • CDockContainerWidget accepts drops and drives the drop overlays from the drag-and-drop event positions instead of QCursor::pos(), which is stale during a drag on Wayland.
  • Dragging the tab or dock-area title bar of a floating widget drags the existing floating window, so it can be docked back into another container.

Platform impact

Every change is guarded by a new ads::internal::isWayland() helper, so X11, Windows and macOS are unaffected. Where a function is shared (e.g. the drop overlay setup, now factored into CDockContainerWidget::showDropOverlays()), the behavior is preserved on the other platforms. I verified the X11 mouse-drag path still works unchanged.

This change is self-contained and does not depend on any other PR. On the non-Wayland paths the existing top-level Qt::Tool overlays are kept exactly as before; only on Wayland are the overlays child widgets.

Requirements and behavioral notes (also updated in README)

  • Requires Qt 6.6.3+ for the xdg_toplevel_drag_v1 support; on older Qt the code is harmless but the window will not follow the cursor, so X11/XWayland is still recommended there.
  • On Wayland: native decorations only (no custom title bar), the real window is dragged instead of a translucent preview, a floating window takes the dock manager's stylesheet when it is created (a later application-wide stylesheet change does not update an already-floating window), and the compositor owns window stacking (no forced stays-on-top).

Testing

Tested on GNOME / Mutter 50.1 with Qt 6.11 (PySide6): undock, move via the decoration, drop on an indicator to re-dock, drop outside to keep floating, re-dock a floating window via its title bar, floating-to-floating drop, and auto-hide undock. The X11/xcb path was regression tested and is unchanged.

Other compositors are not yet covered; on KDE/KWin a benign Qt warning (This plugin supports grabbing the mouse only for popup windows) is emitted by Qt's own drag-and-drop internals and does not block docking.

References

@githubuser0xFFFF

Copy link
Copy Markdown
Owner

Thank you very much for your comprehensive documentation. Could you please tell me, in which way your merge request is related to this one: #837

Do I need to merge both or is your merge request a more comlpete replacement?

Thank you.

@mliberty1

Copy link
Copy Markdown
Author

This PR is related to #837, but they are different. This PR allows undocked windows to be separate from the original window. You can then move widgets between windows to reparent them. It allows Qt ADS to work correctly without crashes or corruption while allowing undocked window resizing. It does somewhat degrade the Qt-ADS experience, but you can accomplish all widget positioning you may want. This PR is also written to only affect Wayland code and deliberately avoid touching code for other platforms.

PR 837 improves the drag & drop widget experience within the original window on Wayland. It does not fix the multiple window experience.

Here's a video attempting to show the differences:
http://download.joulescope.com/other/qt_ads_wayland.mp4
Sorry about the low audio volume!

After playing with this some more, I am working on seeing if we can keep the existing widget drag behavior within the window and only activate this 844 behavior when we need to create a new window.

mliberty1 pushed a commit to jetperch/Qt-Advanced-Docking-System that referenced this pull request Jun 13, 2026
… window

PR githubuser0xFFFF#844 always converts a dock drag into a native compositor platform drag at
the drag threshold on Wayland, because the mouse-tracked drag preview relied on
the (unreliable) global cursor position. That replaces the familiar in-window
rearrange UX with a new top-level window for every drag.

This restores the normal behavior while the cursor stays inside the source
top-level window, and only starts the native platform drag once the cursor
leaves it - as one continuous gesture. The same applies to rearranging widgets
inside an already-floating window.

How it works:
- The in-window phase reuses CFloatingDragPreview. On Wayland the preview is
  rendered as a child of the source window (a Qt::Tool top-level cannot be
  positioned in screen coordinates there) and is driven by the reliable,
  event-supplied global position from the grabbing tab/title-bar instead of
  QCursor::pos(). Drop-target hit-testing/overlays are confined to the source
  container (new CFloatingDragPreview::setSourceContainer()).
- When the cursor leaves the source window, the preview is torn down
  (cancelDraggingSilently()) and a real CFloatingDockContainer is created and
  handed to the existing CFloatingDockContainer::startPlatformDrag(), while the
  press's implicit pointer grab is still held. Implemented for both the tab
  (CDockWidgetTab) and title-bar (CDockAreaTitleBar) drag paths.

startPlatformDrag(): do not wait for the new window to be exposed before
QDrag::exec(). Per xdg_toplevel_drag_v1 the attach offset that places the window
under the cursor is only honoured while the toplevel is still UNMAPPED; show()
creates the xdg_toplevel role but the surface only maps once the event loop is
pumped, so waiting for exposure made the compositor ignore the offset and place
the new window at its default position (screen center). This also removes the
previous 250ms exposure wait.

Impact on non-Wayland platforms (X11/Windows/macOS): none. Every behavioral
change is gated on internal::isWayland():
- The child-widget preview, event-coordinate tracking, source-container
  confinement, boundary-cross conversion, and the preview raise() are all
  isWayland()-gated; otherwise the preview stays a Qt::Tool top-level and the
  existing QCursor::pos()-driven moveFloating() path is used unchanged.
- The drag-decision sites in CDockWidgetTab::startFloating and
  DockAreaTitleBarPrivate::makeAreaFloating drop the old isWayland-only
  "PlatformDrag" branch; the remaining expression is identical to the previous
  value on non-Wayland (where PlatformDrag was always false).
- The CFloatingDragPreview shared-code refactor (moveFloating(QPoint) overload,
  explicit-position overlay calls, finishDragging position source) is
  behavior-preserving off Wayland: the no-arg overlay methods already delegate
  with QCursor::pos(), and the event-position stash used by finishDragging is
  only set on Wayland, so the live QCursor::pos() is still used elsewhere.
- startPlatformDrag() is only ever invoked from isWayland()-gated paths.

See doc/wayland_hybrid_drag_test_plan.md for the manual verification procedure
and the automated-coverage gap (these paths need a live compositor + QDrag).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mliberty1

Copy link
Copy Markdown
Author

I just pushed an additional commit that effectively combines 837 into 844. The result is much nicer, and Wayland feels like a first-class platform for Qt ADS! The PR is implemented in a way that only affects Wayland. Other platforms should be functionally unchanged. Here is a demo:

http://download.joulescope.com/other/qt_ads_wayland_02.mp4

I am happy to squash and force commit if you want a single commit to review and merge. Let me know!


Detailed docs on this second commit:

Summary

This PR adds Wayland support to the docking system. On Wayland a client cannot reposition top-level windows and QCursor::pos() is unreliable, so the normal mouse-tracked drag of a floating window does not work. #844 addresses this by driving cross-window dock drags through the compositor's xdg_toplevel_drag_v1 protocol (CFloatingDockContainer::startPlatformDrag()), with the drop overlays rendered as child widgets so they align correctly on Wayland.

Latest commit: keep the in-window drag preview, go native only when leaving the window

The original approach converted every dock drag into a native platform drag (a new top-level window) the moment the drag threshold was crossed, which replaced the familiar in-window rearrange UX. The latest commit makes the behavior hybrid: while the cursor stays inside the source top-level window, the drag uses the normal in-window CFloatingDragPreview + drop overlays (no new window); only when the cursor leaves the window does it tear down the preview, create a real CFloatingDockContainer, and hand it to startPlatformDrag() — all in one continuous gesture, with the original press's implicit pointer grab still held. This is wired for both the tab (CDockWidgetTab) and title-bar (CDockAreaTitleBar) drag paths, and applies equally to rearranging widgets inside an already-floating window.

On Wayland the in-window preview is rendered as a child of the source window and driven by the reliable event-supplied position from the grabbing widget (not QCursor::pos()), with drop-target hit-testing confined to the source container.

One important protocol detail: xdg_toplevel_drag_v1 only honors the attach offset that places the new window under the cursor while the toplevel is still unmapped. show() creates the xdg_toplevel role but the surface only maps once the event loop is pumped, so startPlatformDrag() deliberately does not wait for exposure before QDrag::exec() — waiting would map the window first and the compositor would then ignore the offset and place the window at its default position (screen center).

Impact on other platforms

None. Every behavioral change is gated on internal::isWayland(). On X11/Windows/macOS the preview stays a Qt::Tool top-level using the existing QCursor::pos()-driven path; the shared-code refactors (a moveFloating(QPoint) overload, explicit-position overlay calls, the finishDragging position source) are behavior-preserving off Wayland (the no-arg overlay methods already delegate with QCursor::pos(), and the event-position stash is only set on Wayland), and startPlatformDrag() is only ever invoked from Wayland-gated paths.

Relationship to #837

#844 and #837 solve the same Wayland problem in overlapping code, and with the latest commit #844 now fully absorbs #837. They remain co-mergeable (a merge produces two resolvable, same-intent conflicts in DockOverlay.cpp and FloatingDragPreview.cpp), but #837 no longer adds anything of substance on top of #844.

#837's real footprint is three files (~48 insertions / 38 deletions):

#837 change Status in #844
DockOverlay.cpp — overlays/cross as child widgets on Wayland; devicePixelRatioF() fix Superseded — #844 implements the same, isWayland()-gated, and keeps the X11 Qt::Tool path
FloatingDragPreview.cpp — preview as a child widget on Wayland Superseded — the latest #844 commit implements this, gated, plus event-coordinate tracking
setWindowOpacity(1) removal in CDockOverlay ctor Moot — in #844 that call already lives in the non-Wayland else branch, so it never runs on Wayland
setWindowOpacity(1) removal in CFloatingDockContainer::finishDragging Near-moot — that path isn't reached in #844's Wayland drag flow; on X11 it sets opacity to its default (1.0), a no-op

In short: the only lines in #837 not already present in #844 are two setWindowOpacity(1) removals, both of which #844 already neutralizes via gating. Adopting #844 means #837 can be dropped without losing any behavior.

@Wing-summer

Copy link
Copy Markdown

Thanks for your hard working on wayland support on linux wayland. This solution is almost fantastic and I think I can close my PR, tested on my KDE wayland.

I find a minor bug: If you drag a docking window out of the main container and make it finally a floating window, sometime you cannot drag another docking window onto the recent newly created floating window.

Another workon maybe a wayland limitation: You can not actually control the position of floating window when it transforms from docking window just now. It's wired when you drag a window out but it has a large distance with you cursor.

@mliberty1

Copy link
Copy Markdown
Author

Hi @Wing-summer - It is working really well for me on Ubuntu 26.04 LTS.

A few things:

  1. Can you provide a picture or video of the issues you see? You might be doing something in a way that I have not tried.
  2. What Linux distro do you use? I have a spare computer that I can use to try to duplicate what you see.

@mliberty1

Copy link
Copy Markdown
Author

I find a minor bug: If you drag a docking window out of the main container and make it finally a floating window, > sometime you cannot drag another docking window onto the recent newly created floating window.

  1. One known issue is that dragging the Window title bar does not drag and drop widgets. You need to drag the Widget tab or Widget title area. The area in the red rectangle does not support drag and drop:
image Is this what you mean?

Another workon maybe a wayland limitation: You can not actually control the position of floating window when it transforms from docking window just now. It's wired when you drag a window out but it has a large distance with you cursor.

  1. The widget maintains it's size when it transforms into a docking window. I am not quite sure how the existing code determines the location of the widget relative to your cursor. I suppose it should keep the same widget location where you started the drag under the cursor. I will investigate.

  2. I noticed another issue. Arrange things so that you have a main window and a docking window, each with a few widgets. When you drag a widget in the docking window, the preview shows up on the main window. It should be on the docking window. I will investigate.

@Wing-summer

Copy link
Copy Markdown

Hi @Wing-summer - It is working really well for me on Ubuntu 26.04 LTS.

A few things:

  1. Can you provide a picture or video of the issues you see? You might be doing something in a way that I have not tried.
  2. What Linux distro do you use? I have a spare computer that I can use to try to duplicate what you see.
  1. There is a video following to reproduce the bug:
    bug.webm
  2. I use CachyOS with KDE Plasma 6.6.5.

@Wing-summer

Copy link
Copy Markdown

I find a minor bug: If you drag a docking window out of the main container and make it finally a floating window, > sometime you cannot drag another docking window onto the recent newly created floating window.

  1. One known issue is that dragging the Window title bar does not drag and drop widgets. You need to drag the Widget tab or Widget title area. The area in the red rectangle does not support drag and drop:

Is this what you mean?

Another workon maybe a wayland limitation: You can not actually control the position of floating window when it transforms from docking window just now. It's wired when you drag a window out but it has a large distance with you cursor.

  1. The widget maintains it's size when it transforms into a docking window. I am not quite sure how the existing code determines the location of the widget relative to your cursor. I suppose it should keep the same widget location where you started the drag under the cursor. I will investigate.
  2. I noticed another issue. Arrange things so that you have a main window and a docking window, each with a few widgets. When you drag a widget in the docking window, the preview shows up on the main window. It should be on the docking window. I will investigate.
  1. Not yet, I've uploaded a video. This problem is wayland limitation, We don't need to focus on it.
  2. I think it's wm behavior. KDE Plasma can restore the window position and size now. But it's far-fetched to think it were the reason.
  3. I noticed too recently. But somehow I think it's not a very big problem.

…window drag

Qt Advanced Docking System could not dock on Wayland: a client cannot position
top-level windows and the global cursor position is unreliable, so the
mouse-tracked floating-widget drag never worked. This adds Wayland docking by
combining two mechanisms, selected by whether the drag stays inside its source
window.

Native cross-window drag (xdg_toplevel_drag_v1):
- CFloatingDockContainer::startPlatformDrag() runs a compositor-driven drag via
  QDrag with the Qt main-window-drag MIME types, so a floating window is moved
  by the compositor and can be dropped onto another window.
- Drop targets handle the drag in CDockContainerWidget dragEnter/Move/Leave/drop
  events, with a recorded drop-candidate fallback for compositors that do not
  deliver a drop event over the dragged window.
- The drop overlays (CDockOverlay) and the in-window drag preview are rendered
  as child widgets of the relevant top-level window, because a Qt::Tool
  top-level cannot be positioned in screen coordinates on Wayland.
- Floating containers use a native window with no parent on Wayland, and the
  DockManager stays-on-top emulation that would recreate window surfaces is
  skipped.

In-window preview, native only when leaving the window:
- While the cursor stays inside the source top-level window, a drag uses the
  familiar in-window CFloatingDragPreview plus drop overlays (no new window),
  driven by the reliable event-supplied position from the grabbing tab or title
  bar and confined to the source container.
- When the cursor leaves the window, the preview is torn down and a real
  CFloatingDockContainer is created and handed to startPlatformDrag() while the
  press's implicit pointer grab is still held - a single continuous gesture.
  This covers both the tab (CDockWidgetTab) and title-bar (CDockAreaTitleBar)
  drag paths and applies to rearranging widgets inside floating windows too.
- The attach offset that places the new window under the cursor is only honored
  while the toplevel is unmapped, so startPlatformDrag() runs QDrag::exec()
  without waiting for exposure, and the caller passes the surface-local grab
  offset explicitly - shifted by the window frame top and left margins - instead
  of deriving it from the unmapped window's (meaningless) geometry, so the
  grabbed content point stays under the cursor on both axes.

Style sheets and overlay lifetime:
- A Wayland floating container has no parent widget, so it does not inherit the
  dock manager's effective style sheet through the widget hierarchy. The style
  sheets along the dock manager parent chain are applied explicitly when the
  window is created, and CDockManager re-applies them on QEvent::StyleChange so
  a floating window keeps matching the docked content when the style sheet
  changes at runtime (an application-wide qApp style sheet is still applied by
  Qt automatically).
- The drop overlays are reparented into the top-level window they are shown over
  and reparented back to the dock manager's window when hidden, so a
  dock-manager-owned overlay is never left as a child of a transient floating
  window that gets destroyed.

Saved layouts restore the docked arrangement, floating-window sizes and their
maximized/normal state, but not floating-window or main-window positions:
Wayland does not let a client position its own top-level windows, so the
compositor decides where restored windows appear.

Non-Wayland platforms (X11, Windows, macOS) are unaffected: every behavioral
change is gated on internal::isWayland(), and the shared drag-preview code keeps
using QCursor::pos() and the existing window positioning off Wayland.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mliberty1
mliberty1 force-pushed the wayland-standalone branch from 90ee526 to cdca4b2 Compare June 14, 2026 18:03
@mliberty1

mliberty1 commented Jun 14, 2026

Copy link
Copy Markdown
Author

I just did a force push, which further improves this PR.

  1. Thanks for the video @Wing-summer ! This is the expected behavior, and I am not sure how we would fix it. The widget stays internal to the Window as long as the cursor stays inside. As soon as you drag the widget out of the Window, it gets its own Dock Window. Alternatively, if you release the drag like you did on a non-docking region, it also gets it's own Dock Window. Therefore, when you dragged the widget to the Dock Window situated over the Main Window, the widget stays in the main Window. I can't duplicate this behavior on Ubuntu since the Dock Widget ends up behind the Main Window when I click on the widget.
  2. Fixed! The cursor now stays over the same location on the widget where you started the drag. It may be off by a few pixels due to window styling, but it is very close.
  3. Fixed. The preview now goes to the widget's current window.
  4. Fixed stylesheet cascading, which was previously partially implemented.
  5. Confirmed that Window position restore is not possible on Wayland and updated README.
  6. Fixed use-after-free crash.

Overall, this PR makes Wayland very well supported by Qt ADS!

@githubuser0xFFFF

Copy link
Copy Markdown
Owner

@mliberty1 Thank your very much for your fantastic work. This is a really massive improvement for Linux users. @Wing-summer Thank you very much for reviewing the merge request,

@githubuser0xFFFF
githubuser0xFFFF merged commit f2db7ca into githubuser0xFFFF:master Jun 15, 2026
@mliberty1

Copy link
Copy Markdown
Author

@githubuser0xFFFF - Thanks for all your work on this great project, being so responsive, and merging this PR so quickly! I think a bunch of ADS users have struggled using xcb (X11 emulation). My customers will be happy that the Joulescope UI will soon have proper Wayland support!

@githubuser0xFFFF

Copy link
Copy Markdown
Owner

@mliberty1 Thank you again for your outstanding contribution. Today I released ADS 5.0.0 with all the contributed new features. I highlighted your contribution in the New and Noteworthy section and added a showcase section for your Joulescope application.

@mliberty1

Copy link
Copy Markdown
Author

Excellent, and thank you for your work on this project, @githubuser0xFFFF !

ksylvan added a commit to story-wizard/Qt-Advanced-Docking-System that referenced this pull request Jul 3, 2026
…ory)

Brings in 14 upstream commits incl. dark mode support (githubuser0xFFFF#842), Wayland
drag-and-drop with in-window preview (githubuser0xFFFF#844), auto-hide title bar fix
(githubuser0xFFFF#843), Linux floating-raise fix (githubuser0xFFFF#840), LP360 double-removal fix
(githubuser0xFFFF#838), and lazy CDockWidgetTab creation for Python factories (githubuser0xFFFF#848).

Conflict resolutions:
- DockManager.cpp: keep both new includes (QTimer ours, QStyleHints upstream)
- DockOverlay.cpp: keep fork's AutoHideFeatureEnabled guard structure and
  half-panel edge-band fall-throughs; adopt upstream's GlobalPos parameter
  (new dropAreaUnderCursor(QPoint) overload) instead of QCursor::pos()
- FloatingDockContainer.cpp: line-ending false conflict (fork stores LF,
  upstream CRLF) re-merged content-wise; kept macOS escape-key polling,
  took upstream's Wayland ctor (isWayland() null-parent) + comment
- FloatingDragPreview.cpp: kept fork's dropOverlaysEnabled() early-out;
  adopted upstream's SourceContainer/Wayland container-picking loop

Validated: full build clean; QuadrantHitTestTest + OverlayGateTest pass.
@ciros88

ciros88 commented Jul 23, 2026

Copy link
Copy Markdown

there is a reason why isWayland() function is based on

return QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive);

instead of something like

return qEnvironmentVariable("XDG_SESSION_TYPE") == "wayland";

?

anyway thanks for you great work

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants